package org.checkerframework.afu.scenelib.io;

import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ArrayAccessTree;
import com.sun.source.tree.ArrayTypeTree;
import com.sun.source.tree.AssertTree;
import com.sun.source.tree.AssignmentTree;
import com.sun.source.tree.BinaryTree;
import com.sun.source.tree.BlockTree;
import com.sun.source.tree.CaseTree;
import com.sun.source.tree.CatchTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.CompoundAssignmentTree;
import com.sun.source.tree.ConditionalExpressionTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.EnhancedForLoopTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.InstanceOfTree;
import com.sun.source.tree.IntersectionTypeTree;
import com.sun.source.tree.LabeledStatementTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MemberReferenceTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.ParameterizedTypeTree;
import com.sun.source.tree.ParenthesizedTree;
import com.sun.source.tree.ReturnTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.ThrowTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.TryTree;
import com.sun.source.tree.TypeCastTree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.UnaryTree;
import com.sun.source.tree.UnionTypeTree;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.source.tree.WildcardTree;
import com.sun.source.util.SimpleTreeVisitor;
import com.sun.source.util.TreePath;
import com.sun.tools.javac.code.Symbol.ClassSymbol;
import com.sun.tools.javac.tree.JCTree;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.lang.model.element.Name;
import org.checkerframework.afu.annotator.find.CaseUtils;
import org.checkerframework.afu.scenelib.util.JVMNames;
import org.checkerframework.afu.scenelib.util.coll.WrapperMap;

/** Cache of {@code ASTPath} data for the nodes of a compilation unit tree. */
public class ASTIndex extends WrapperMap<Tree, ASTRecord> {
  // single-item cache
  private static Tree cachedRoot = null;
  private static Map<Tree, ASTRecord> cachedIndex = null;
  private static final int EXPECTED_SIZE = 128;

  private final CompilationUnitTree cut;
  private final Map<String, Map<String, List<String>>> formals;

  /**
   * Maps source trees in compilation unit to corresponding AST paths.
   *
   * @param root compilation unit to be indexed
   * @return map of trees in compilation unit to AST paths
   */
  public static Map<Tree, ASTRecord> indexOf(CompilationUnitTree root) {
    if (cachedRoot == null || !cachedRoot.equals(root)) {
      cachedRoot = root;
      cachedIndex = new ASTIndex(root);
    }
    return cachedIndex;
  }

  private ASTIndex(CompilationUnitTree root) {
    super(HashBiMap.<Tree, ASTRecord>create(EXPECTED_SIZE));
    cut = root;
    formals = new HashMap<>();

    cut.accept(new AstIndexVisitor(), null);
  }

  public static ASTRecord getASTPath(CompilationUnitTree cut, Tree node) {
    return indexOf(cut).get(node);
  }

  public static TreePath getTreePath(CompilationUnitTree cut, ASTRecord rec) {
    Tree node = getNode(cut, rec);
    return node == null ? null : TreePath.getPath(cut, node);
  }

  public static Tree getNode(CompilationUnitTree cut, ASTRecord rec) {
    Map<Tree, ASTRecord> fwdIndex = ((ASTIndex) indexOf(cut)).back;
    Map<ASTRecord, Tree> revIndex = ((BiMap<Tree, ASTRecord>) fwdIndex).inverse();
    ExpressionTree et = cut.getPackageName();
    String pkg = et == null ? "" : et.toString();
    if (!pkg.isEmpty() && rec.className.indexOf('.') < 0) {
      String className = pkg + "." + rec.className;
      rec = new ASTRecord(cut, className, rec.methodName, rec.varName, rec.astPath);
    }
    return revIndex.get(rec);
  }

  public static String getParameterName(
      CompilationUnitTree cut, String className, String methodName, int index) {
    try {
      ASTIndex ai = (ASTIndex) ASTIndex.indexOf(cut);
      return ai.formals.get(className).get(methodName).get(index);
    } catch (NullPointerException ex) {
      return null;
    }
  }

  @SuppressWarnings("EmptyCatch") // TODO
  public static Integer getParameterIndex(
      CompilationUnitTree cut, String className, String methodName, String varName) {
    if (cut != null && className != null && methodName != null && varName != null) {
      // If `varName` is already a number, return it
      try {
        return Integer.valueOf(varName);
      } catch (NumberFormatException ex) {
        // Fall through in order to check name.
      }
      // otherwise, look through parameter list for string
      try {
        ASTIndex ai = (ASTIndex) ASTIndex.indexOf(cut);
        List<String> names = ai.formals.get(className).get(methodName);
        int i = 0;
        for (String name : names) {
          if (varName.equals(name)) {
            return i;
          }
          ++i;
        }
      } catch (NullPointerException ex) {
        // Not found.
        // TODO: It would be cleaner to check for null above than to catch the exception here.
      }
    }
    // not found
    return null;
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder();
    for (Map.Entry<Tree, ASTRecord> entry : entrySet()) {
      sb.append(entry.getKey().toString().replaceAll("\\s+", " "))
          .append(" # ")
          .append(entry.getValue())
          .append("\n");
    }
    return sb.toString();
  }

  // This is an instance class because it accesses `cut` and `formals`.
  // The visitor implementation is slightly complicated by the
  // inclusion of information from both parent and child nodes in each
  // ASTEntry.  The pattern for most node types is to call save() and
  // saveAll() as needed to handle the node's descendants and finally
  // to invoke defaultAction() to save the entry for the current node.
  // (If the JVM could take advantage of tail recursion, it would be
  // better to save the current node's entry first, at a small cost to
  // the clarity of the code.)
  private class AstIndexVisitor extends SimpleTreeVisitor<Void, ASTRecord> {
    private final Deque<Integer> counters = new ArrayDeque<Integer>();
    private String inMethod = null;

    private void save(Tree node, ASTRecord rec, Kind kind, String sel) {
      if (node != null) {
        node.accept(this, rec.extend(kind, sel));
      }
    }

    private void save(Tree node, ASTRecord rec, Kind kind, String sel, int arg) {
      if (node != null) {
        node.accept(this, rec.extend(kind, sel, arg));
      }
    }

    private void saveAll(Iterable<? extends Tree> nodes, ASTRecord rec, Kind kind, String sel) {
      if (nodes != null) {
        int i = 0;
        for (Tree node : nodes) {
          save(node, rec, kind, sel, i++);
        }
      }
    }

    /**
     * Save a class.
     *
     * @param node the class
     */
    private void saveClass(ClassTree node) {
      String className = ((JCTree.JCClassDecl) node).sym.flatname.toString();
      ASTRecord rec = new ASTRecord(cut, className, null, null, ASTPath.empty());
      counters.push(0);
      node.accept(this, rec);
      counters.pop();
    }

    @Override
    public Void defaultAction(Tree node, ASTRecord rec) {
      switch (node.getKind()) {
        case BREAK:
        case COMPILATION_UNIT:
        case CONTINUE:
        case IMPORT:
        case MODIFIERS:
          break; // not handled
        default:
          put(node, rec);
      }
      return null;
    }

    @Override
    public Void visitAnnotatedType(AnnotatedTypeTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(node.getAnnotations(), rec, kind, ASTPath.ANNOTATION);
      save(node.getUnderlyingType(), rec, kind, ASTPath.UNDERLYING_TYPE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitAnnotation(AnnotationTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getAnnotationType(), rec, kind, ASTPath.TYPE);
      saveAll(node.getArguments(), rec, kind, ASTPath.ARGUMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitMethodInvocation(MethodInvocationTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(node.getTypeArguments(), rec, kind, ASTPath.TYPE_ARGUMENT);
      save(node.getMethodSelect(), rec, kind, ASTPath.METHOD_SELECT);
      saveAll(node.getArguments(), rec, kind, ASTPath.ARGUMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitAssert(AssertTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getDetail(), rec, kind, ASTPath.DETAIL);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitAssignment(AssignmentTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getVariable(), rec, kind, ASTPath.VARIABLE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitCompoundAssignment(CompoundAssignmentTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getVariable(), rec, kind, ASTPath.VARIABLE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitBinary(BinaryTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getLeftOperand(), rec, kind, ASTPath.LEFT_OPERAND);
      save(node.getRightOperand(), rec, kind, ASTPath.RIGHT_OPERAND);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitBlock(BlockTree node, ASTRecord rec) {
      Iterable<? extends Tree> nodes = node.getStatements();
      if (nodes != null) {
        int i = 0;
        for (Tree stmt : nodes) {
          if (ASTPath.isClassEquiv(stmt.getKind())) {
            saveClass((ClassTree) stmt);
          } else {
            save(stmt, rec, node.getKind(), ASTPath.STATEMENT, i);
          }
          ++i;
        }
      }
      return defaultAction(node, rec);
    }

    @Override
    public Void visitCase(CaseTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(CaseUtils.caseTreeGetExpressions(node), rec, kind, ASTPath.EXPRESSION);
      saveAll(node.getStatements(), rec, kind, ASTPath.STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitCatch(CatchTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getBlock(), rec, kind, ASTPath.BLOCK);
      save(node.getParameter(), rec, kind, ASTPath.PARAMETER);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitClass(ClassTree node, ASTRecord rec) {
      Kind kind = Tree.Kind.CLASS; // use for all class-equivalent kinds
      int i = 0;
      formals.put(rec.className, new HashMap<>());
      if (node.getSimpleName().length() > 0) {
        // don't save exts/impls for anonymous inner class
        save(node.getExtendsClause(), rec, kind, ASTPath.BOUND, -1);
        saveAll(node.getImplementsClause(), rec, kind, ASTPath.BOUND);
      }
      saveAll(node.getTypeParameters(), rec, kind, ASTPath.TYPE_PARAMETER);
      for (Tree member : node.getMembers()) {
        if (member instanceof BlockTree) {
          save(member, rec, kind, ASTPath.INITIALIZER, i++);
        } else if (ASTPath.isClassEquiv(member.getKind())) {
          String className = ((JCTree.JCClassDecl) member).sym.flatname.toString();
          member.accept(this, new ASTRecord(cut, className, null, null, ASTPath.empty()));
        } else {
          member.accept(this, rec);
        }
      }
      return defaultAction(node, rec);
    }

    @Override
    public Void visitConditionalExpression(ConditionalExpressionTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getFalseExpression(), rec, kind, ASTPath.FALSE_EXPRESSION);
      save(node.getTrueExpression(), rec, kind, ASTPath.TRUE_EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitDoWhileLoop(DoWhileLoopTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getStatement(), rec, kind, ASTPath.STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitExpressionStatement(ExpressionStatementTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitEnhancedForLoop(EnhancedForLoopTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getVariable(), rec, kind, ASTPath.VARIABLE);
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getStatement(), rec, kind, ASTPath.STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitForLoop(ForLoopTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(node.getInitializer(), rec, kind, ASTPath.INITIALIZER);
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getStatement(), rec, kind, ASTPath.STATEMENT);
      saveAll(node.getUpdate(), rec, kind, ASTPath.UPDATE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitIf(IfTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getThenStatement(), rec, kind, ASTPath.THEN_STATEMENT);
      save(node.getElseStatement(), rec, kind, ASTPath.ELSE_STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitArrayAccess(ArrayAccessTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getIndex(), rec, kind, ASTPath.INDEX);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitLabeledStatement(LabeledStatementTree node, ASTRecord rec) {
      save(node.getStatement(), rec, node.getKind(), ASTPath.STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitMethod(MethodTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      VariableTree rcvr = node.getReceiverParameter();
      ModifiersTree mods = node.getModifiers();
      List<? extends VariableTree> params = node.getParameters();
      String outMethod = inMethod;
      inMethod = JVMNames.getJVMMethodSignature(node);
      rec = new ASTRecord(cut, rec.className, inMethod, null, ASTPath.empty());
      if (mods != null) {
        save(mods, rec, kind, ASTPath.MODIFIERS);
      }
      if (rcvr != null) {
        rcvr.accept(this, rec.extend(kind, ASTPath.PARAMETER, -1));
      }
      if (params != null && !params.isEmpty()) {
        Map<String, List<String>> map = formals.get(rec.className);
        List<String> names = new ArrayList<>(params.size());
        int i = 0;
        map.put(inMethod, names);
        for (VariableTree param : params) {
          if (param != null) {
            names.add(param.getName().toString());
            param.accept(this, rec.extend(Tree.Kind.METHOD, ASTPath.PARAMETER, i++));
          }
        }
      }
      save(node.getReturnType(), rec, kind, ASTPath.TYPE);
      saveAll(node.getTypeParameters(), rec, kind, ASTPath.TYPE_PARAMETER);
      // save(node.getReceiverParameter(), rec, kind, ASTPath.PARAMETER, -1);
      // saveAll(node.getParameters(), rec, kind, ASTPath.PARAMETER);
      saveAll(node.getThrows(), rec, kind, ASTPath.THROWS);
      save(node.getBody(), rec, kind, ASTPath.BODY);
      inMethod = outMethod;
      return defaultAction(node, rec);
    }

    @Override
    public Void visitModifiers(ModifiersTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(node.getAnnotations(), rec, kind, ASTPath.ANNOTATION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitNewArray(NewArrayTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      Tree type = node.getType();
      int n = node.getDimensions().size();
      do {
        save(type, rec, kind, ASTPath.TYPE, n);
      } while (--n > 0);
      saveAll(node.getDimensions(), rec, kind, ASTPath.DIMENSION);
      saveAll(node.getInitializers(), rec, kind, ASTPath.INITIALIZER);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitNewClass(NewClassTree node, ASTRecord rec) {
      JCTree.JCClassDecl classBody = (JCTree.JCClassDecl) node.getClassBody();
      Kind kind = node.getKind();
      save(node.getEnclosingExpression(), rec, kind, ASTPath.ENCLOSING_EXPRESSION);
      saveAll(node.getTypeArguments(), rec, kind, ASTPath.TYPE_ARGUMENT);
      save(node.getIdentifier(), rec, kind, ASTPath.IDENTIFIER);
      saveAll(node.getArguments(), rec, kind, ASTPath.ARGUMENT);
      if (classBody != null) {
        Name name = classBody.getSimpleName();
        String className = null;
        if (name == null || name.toString().isEmpty()) {
          int i = counters.pop();
          counters.push(++i);
          className = rec.className + "$" + i;
        } else {
          ClassSymbol sym = classBody.sym;
          if (sym != null && sym.flatname != null && sym.flatname.length() > 0) {
            // Use javac's flatname rather than parsing toString() output.
            className = sym.flatname.toString();
          } else {
            className = rec.className + "$" + name;
          }
        }
        counters.push(0);
        classBody.accept(this, new ASTRecord(cut, className, null, null, ASTPath.empty()));
        counters.pop();
      }
      return defaultAction(node, rec);
    }

    @Override
    public Void visitLambdaExpression(LambdaExpressionTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      String outMethod = inMethod;
      List<? extends VariableTree> params = node.getParameters();
      if (params != null) {
        int i = 0;
        for (VariableTree vt : params) {
          ASTRecord newRec = rec.extend(kind, ASTPath.PARAMETER, i++);
          save(vt.getType(), newRec, Tree.Kind.VARIABLE, ASTPath.TYPE);
          save(vt.getInitializer(), newRec, Tree.Kind.VARIABLE, ASTPath.INITIALIZER);
          defaultAction(vt, newRec);
        }
      }
      save(node.getBody(), rec, kind, ASTPath.BODY);
      inMethod = outMethod;
      return defaultAction(node, rec);
    }

    @Override
    public Void visitParenthesized(ParenthesizedTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitReturn(ReturnTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitMemberSelect(MemberSelectTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitMemberReference(MemberReferenceTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getQualifierExpression(), rec, kind, ASTPath.QUALIFIER_EXPRESSION);
      saveAll(node.getTypeArguments(), rec, kind, ASTPath.TYPE_ARGUMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitSwitch(SwitchTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      saveAll(node.getCases(), rec, kind, ASTPath.CASE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitSynchronized(SynchronizedTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getBlock(), rec, kind, ASTPath.BLOCK);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitThrow(ThrowTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitCompilationUnit(CompilationUnitTree node, ASTRecord rec) {
      for (Tree tree : node.getTypeDecls()) {
        if (ASTPath.isClassEquiv(tree.getKind())) {
          saveClass((ClassTree) tree);
        }
      }
      return null;
    }

    @Override
    public Void visitTry(TryTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      saveAll(node.getResources(), rec, kind, ASTPath.RESOURCE);
      save(node.getBlock(), rec, kind, ASTPath.BLOCK);
      saveAll(node.getCatches(), rec, kind, ASTPath.CATCH);
      save(node.getFinallyBlock(), rec, kind, ASTPath.FINALLY_BLOCK);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitParameterizedType(ParameterizedTypeTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getType(), rec, kind, ASTPath.TYPE);
      saveAll(node.getTypeArguments(), rec, kind, ASTPath.TYPE_ARGUMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitUnionType(UnionTypeTree node, ASTRecord rec) {
      saveAll(node.getTypeAlternatives(), rec, node.getKind(), ASTPath.TYPE_ALTERNATIVE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitIntersectionType(IntersectionTypeTree node, ASTRecord rec) {
      saveAll(node.getBounds(), rec, node.getKind(), ASTPath.BOUND);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitArrayType(ArrayTypeTree node, ASTRecord rec) {
      save(node.getType(), rec, node.getKind(), ASTPath.TYPE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitTypeCast(TypeCastTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getType(), rec, kind, ASTPath.TYPE);
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitTypeParameter(TypeParameterTree node, ASTRecord rec) {
      saveAll(node.getBounds(), rec, node.getKind(), ASTPath.BOUND);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitInstanceOf(InstanceOfTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getExpression(), rec, kind, ASTPath.EXPRESSION);
      save(node.getType(), rec, kind, ASTPath.TYPE);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitUnary(UnaryTree node, ASTRecord rec) {
      save(node.getExpression(), rec, node.getKind(), ASTPath.EXPRESSION);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitVariable(VariableTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      if (rec.methodName == null) { // member field
        rec =
            new ASTRecord(
                cut, rec.className, rec.methodName, node.getName().toString(), rec.astPath);
      }
      save(node.getType(), rec, kind, ASTPath.TYPE);
      save(node.getInitializer(), rec, kind, ASTPath.INITIALIZER);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitWhileLoop(WhileLoopTree node, ASTRecord rec) {
      Kind kind = node.getKind();
      save(node.getCondition(), rec, kind, ASTPath.CONDITION);
      save(node.getStatement(), rec, kind, ASTPath.STATEMENT);
      return defaultAction(node, rec);
    }

    @Override
    public Void visitWildcard(WildcardTree node, ASTRecord rec) {
      save(node.getBound(), rec, node.getKind(), ASTPath.BOUND);
      return defaultAction(node, rec);
    }
  }
}
